Skip to content

Fix blank code blocks in assistant - #5772

Open
backspace wants to merge 11 commits into
mainfrom
assistant-blank-blocks-cs-12491
Open

Fix blank code blocks in assistant#5772
backspace wants to merge 11 commits into
mainfrom
assistant-blank-blocks-cs-12491

Conversation

@backspace

@backspace backspace commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

I’ve been seeing gaps like this:

image

it comes with an infinite series of 404s, which actually managed to take down staging:

image
Claude’s explanation A code block's arguments are recomputed on every invalidation of the room resource, which during streaming arrives continuously. `modify` treated each one as a reason to start over: it cleared any error and performed the load again, and because that load is a restartable task, performing it cancelled the request in flight.

Two things followed. The resource sat holding no code and no error, which is the one state the template renders as nothing at all — a code block with a header, an editor and an apply button in the DOM but no content in any of them. And since cancelling the task does not cancel the request behind it, every restart left its fetch running and started another, so a patch creating a new file — where a 404 is the expected answer — asked the realm for that file thousands of times. The requests then contended with each other, so each load took longer, so more of them were cancelled before they could finish.

Restart only when the file or the patch actually changes; otherwise leave a load that is already under way alone. Give the load an abort signal so a superseded one stops rather than merely having its answer discarded, and thread that signal through getSource. Keep the in-flight flag untracked, since consuming tracked state in modify would re-enter it on every change.

A code block's arguments are recomputed on every invalidation of the room
resource, which during streaming arrives continuously. `modify` treated each
one as a reason to start over: it cleared any error and performed the load
again, and because that load is a restartable task, performing it cancelled the
request in flight.

Two things followed. The resource sat holding no code and no error, which is
the one state the template renders as nothing at all — a code block with a
header, an editor and an apply button in the DOM but no content in any of them.
And since cancelling the task does not cancel the request behind it, every
restart left its fetch running and started another, so a patch creating a new
file — where a 404 is the expected answer — asked the realm for that file
thousands of times. The requests then contended with each other, so each load
took longer, so more of them were cancelled before they could finish.

Restart only when the file or the patch actually changes; otherwise leave a
load that is already under way alone. Give the load an abort signal so a
superseded one stops rather than merely having its answer discarded, and thread
that signal through getSource. Keep the in-flight flag untracked, since
consuming tracked state in `modify` would re-enter it on every change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Preview deployments

Host Test Results

    1 files  ±    0      1 suites  ±0   1h 37m 16s ⏱️ + 1h 31m 55s
3 187 tests +2 947  3 173 ✅ +2 934  14 💤 +13  0 ❌ ±0 
3 201 runs  +2 961  3 187 ✅ +2 948  14 💤 +13  0 ❌ ±0 

Results for commit 3391d1d. ± Comparison against earlier commit ca418d6.

Realm Server Test Results

    1 files  ±0      1 suites  ±0   15m 50s ⏱️ + 3m 27s
2 174 tests ±0  2 174 ✅ ±0  0 💤 ±0  0 ❌ ±0 
2 254 runs  ±0  2 254 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 3391d1d. ± Comparison against earlier commit ca418d6.

backspace and others added 2 commits August 13, 2026 16:01
Rendering it for any truthy file URL reached the header with values that are
not URLs — the model names the file it is patching, and what it writes is not
always parseable. `fileName` constructs a URL from it unguarded, so the getter
threw and took the whole message render down with it.

A patch that failed does not need the header anyway: the footer's alert already
speaks for it, and the empty block this was meant to fix is the pending one.
Restrict the header to that case, and stop `fileName` from throwing so a header
that is merely wrong cannot break the render. The fallback matters beyond this
branch — the standard code block renders the same header for any truthy file
url.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three behaviours shipped without tests, each one the kind that breaks quietly.

A superseded load has to abort its request, since cancelling the task leaves
the fetch running and only discards the answer — the difference between a
bounded restart and a request storm is invisible from the rendered output.

A patch whose diff has not arrived has to say so. Asserting only that the
loading state is absent once settled would pass just as well if it never
rendered at all, which is the bug this was written to prevent.

And a file name that is not a URL has to reach the header without throwing.
The model names the file it is patching and what it writes is not always
parseable; constructing a URL from it unguarded took down the whole message.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@backspace
backspace marked this pull request as ready for review August 17, 2026 17:32

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fe6879a19f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

this.abortController = abortController;
this.loadInFlight = true;
try {
await this.loadDiff(abortController.signal);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep cancellable work in the task body

When the inputs change after getSource resolves but while ApplySearchReplaceBlockTool.execute() is awaiting its module load, cancelling this restartable task stops only the outer task at this await; loadDiff() is a separate ordinary async operation, and the abort signal no longer stops it after the fetch. The superseded operation can therefore later overwrite originalCode, modifiedCode, or errorMessage belonging to the newer patch (and can also mutate after destruction), displaying or copying a stale diff. Keep the state-mutating awaits directly in the task or verify the controller/signal after every await before assigning resource state.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Real bug, and precisely diagnosed. Fixed in f3d3f89.

You're right about the boundary: loadDiff is an ordinary async function, so cancelling the task only stops things at the await inside the task body. Past that, the abort signal reaches the fetch and nothing else — ApplySearchReplaceBlockTool.execute() awaits a module load with no knowledge of it.

Worth naming the exact hole, because it wasn't where I'd assumed. The catch after getSource already had if (signal.aborted) return, so I'd covered the fetch rejected mid-flight case. What was uncovered was the success path — a fetch that completed before the supersession, followed by an execute() that spans it. That run then resumed and assigned originalCode, modifiedCode, and errorMessage belonging to the newer patch.

Took the second of your two options — verify after every await — rather than moving the work into the task body, because it holds regardless of how ember-concurrency cancels an async task, and I didn't want the correctness of this to rest on my reading of those internals.

let result = await this.cardService.getSource(new URL(fileUrl), { signal });
if (signal.aborted) return;
originalCode = result.status === 404 ? '' : result.content;

…and the same guard on both the resolve and reject paths of execute(). One related change your comment implies: the patch is now applied against originalCode — the value this load fetched — instead of this.originalCode, which by then may belong to a different patch entirely.

The destruction case you mention falls out of the same guard, since the destructor aborts the controller.

Added a regression test that forces the ordering deterministically: two loads started with a deferred getSource, the newer one released and allowed to render its diff, then the abandoned one released afterwards. It asserts the diff still shows the newer replacement. Against the previous code the late load overwrites it.

Not verified locally — Colima is down here, so CI is exercising it.

backspace and others added 5 commits August 17, 2026 13:47
The load runs outside the task, so cancelling the task stops it only at the
await inside the task body. Past that point the work carries on regardless, and
the abort signal reaches the fetch and nothing else — so a load superseded while
awaiting the patch tool resumed later and wrote its answer over the state
belonging to the patch that replaced it, putting a stale diff on screen and
mutating a resource that may already be gone.

Check the signal after each await before writing anything, and apply the patch
against the code this load fetched rather than whatever the resource holds by
the time it resolves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The assertion read `view-lines[2]`, copied from a test that renders several
editors. This one renders a single diff, so that index held nothing, the
predicate was never true, and the wait ran to its timeout instead of failing on
anything real.

Search every pane for the replacement instead, and wait on the insert
decoration the way the neighbouring diff test already does. The abandoned
patch's replacement is now asserted absent too, which is the property the test
exists to hold.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reading the replacement back out of monaco made the test depend on how the diff
editor lays out its panes and which decorations it emits for a one-line change
— twice that guess was wrong, and both times it surfaced as a wait running to
its timeout rather than as anything about the behaviour under test.

Give the abandoned load a search string the file does not contain, so finishing
is something it cannot do quietly: it reports that the patch would not apply.
The property then reads directly off the absence of that error, and the only
DOM this test touches is the diff container and the alert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@richardhjtan richardhjtan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] I reviewed this as a reactivity fix rather than a rendering one: the interesting questions are what modify now entangles with, whether the abort actually reaches the network (the whole cost argument rests on that), and whether the new early return can leave the resource in a state nothing renders — which is the bug being fixed, so re-introducing a variant of it would be the expensive mistake. Everything below was traced against the checked-out branch.

Bottom line: the diagnosis and the fix are both right, and the two claims the change most depends on hold up under tracing — the abort really does reach the socket, and the early return really is exhaustive. No blocking issues. Five things worth acting on, the first being a latent invalidation loop that is currently unreachable only because of two guards in another file.

What lands right

  • The abort reaches the network, which is the claim the "took down staging" half of this rests on. card-service.getSource passes signal into network.authedFetch, which is fetcher(...); fetcher builds new Request(urlOrRequest, init) — carrying the signal — hands that same Request object down the middleware chain unchanged, and calls fetchImplementation(onwardReq.clone()) at the bottom, and Request.clone() preserves the signal. The redirect path in simulateNetworkBehaviors re-passes the original request as init, so it survives that too. Nothing in the chain reconstructs the request from parts and drops it.
  • The early return is exhaustive. After modify returns, the resource is in exactly one of three states — loaded, failed, or loading — and every path either sets one or performs a load. That is what makes the new loading branch in the template a genuine fix rather than a fourth state with a spinner on it.
  • The restart condition compares the right things. searchReplaceBlock is only populated once isCompleteSearchReplaceBlock passes, so it is a stable string after a block completes and the !== compares by value — a rebuilt CodeData carrying identical text is correctly not a change. The test pins this.
  • The ownership guard in the task's finally is right about ember-concurrency's ordering. A restartable task cancels the previous instance asynchronously, so a superseded run's finally can land after the newer run has already claimed abortController and set the flag. Comparing identity rather than clearing unconditionally is the correct handling, and it is not obvious enough to leave uncommented — good that it isn't.
  • Applying the patch against the load's own originalCode rather than this.originalCode is the subtle half of the earlier fix and it is right: the local is the only value guaranteed to belong to the same patch as searchReplaceBlock.

Recommendations

  1. Add codePatchStatus === 'applied' to the early-return condition. It is the one exit from loadDiff that leaves neither data nor an error, so it is the one input that turns this modify into a self-invalidating loop — currently unreachable only because both consumers happen to exclude it. Mechanism in the thread on that line. Non-blocking, four tokens.
  2. Reword the loadInFlight comment: modify does consume tracked state two lines below, and the property that makes that safe (every read is on a returning path) is the one worth writing down. Non-blocking.
  3. Add the failing-load arm to the re-render test — a load that errors must also be fetched once and left alone, and that arm is what the second half of the report describes. Non-blocking, cheapest of these.
  4. The malformed-URL guard stops at fileName; the same string reaches new URL again through this.file.sourceUrl in the attached-file menu's openInCodeMode. Click-time rather than render-time, so it does not undo the fix. Follow-up.
  5. Drop the always-true {{#if @codeData.fileUrl}} around the header in the loading branch. Non-blocking.

Adjacent, out of scope

  • The codePatchStatus === 'applied' branch at the top of loadDiff is dead code as the app stands — no consumer reads this resource while the status is applied. Its comment is genuinely valuable (it records a deliberate product decision about not re-fetching attached files for big chats), so it should not simply be deleted; but it is currently documentation living in an unreachable branch, and recommendation 1 is what makes that state explicit rather than incidental.
  • getSource now takes an options bag that only this caller uses. Fine as it stands; worth knowing that the other call sites get no cancellation, so a slow source fetch elsewhere still runs to completion after its consumer is gone. Not this change's problem, but it is the same class of leak this one just closed.

Generated by Claude Code

Comment on lines +68 to +72
if (!inputsChanged) {
if (this.isDataLoaded || this.errorMessage || this.loadInFlight) {
return;
}
} else {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] This early return is correct today, and it is one line away from being an infinite invalidation loop. The thing that keeps it safe lives in a different file and is not written down anywhere.

Background — why reading tracked state here is different from reading it anywhere else. ember-modify-based-class-resource's manager builds the resource inside createCache(() => { …; instance.modify(positional, named); return instance }) (dist/core/class-based/manager.js). Everything consumed during that computation — including tracked state read inside modify — entangles with the cache. Dirty it and the next getValue re-runs modify. this.isDataLoaded reads the tracked originalCode and modifiedCode; this.errorMessage is tracked too. So this line entangles all three, and load writes all three.

That combination is safe here for one reason only: every path that reads them returns without writing. Data loaded → return. Error set → return. Load in flight → return. The only way through to load.perform() is with all three falsy, and every terminal state loadDiff can reach sets one of them.

Except one. loadDiff opens with if (codePatchStatus === 'applied'), which sets originalCode = null, modifiedCode = null, and returns — the one exit that leaves neither data nor an error. Reach modify in that state with unchanged inputs and it goes: read isDataLoaded (false) → read errorMessage (null) → load.perform(). Ember-concurrency starts a task instance synchronously on perform, and loadDiff's applied branch has no await before its return, so those two writes land inside the same cache computation that just read them. In a debug build that is the backtracking assertion; in production it is the cache invalidating itself on every read.

Why it doesn't fire today. I traced every consumer of this resource before writing the above. There are exactly two, both in aibot-message.gts, and both exclude applied:

  • codePatchErrorMessage returns null at its first branch when the status is applied, before it touches this.codeDiffResource.
  • The template reads codeDiffResource.isDataLoaded / isLoadingDiff only inside the {{else}} of {{#if this.isAppliedOrIgnoredCodePatch}}, and that getter is codePatchStatus === 'applied' || !isLastAssistantMessage.

So modify is never called while the status is applied, the branch at loadDiff's top is unreachable from here, and the loop cannot start. That is a real invariant, held entirely by two guards in a component two files away, with nothing in this file recording it.

The fix is one clause, and it also makes the dead branch in loadDiff honest:

Suggested change
if (!inputsChanged) {
if (this.isDataLoaded || this.errorMessage || this.loadInFlight) {
return;
}
} else {
if (!inputsChanged) {
if (
this.isDataLoaded ||
this.errorMessage ||
this.loadInFlight ||
codePatchStatus === 'applied'
) {
return;
}

With that, applied is a terminal state of modify rather than a state load is asked to reach and then abandon, and the guard stops depending on a consumer's branch structure. The codePatchStatus === 'applied' block in loadDiff can then either go or stay as documentation — but it should not be the thing standing between this and a loop.

Scope. Latent, not live — nothing in the current app reaches it. Non-blocking, but it costs four tokens to remove and the failure mode if someone later renders a loading state for an applied patch is a render loop, not a wrong pixel.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Taken, in ba5f454 — and the trace is right down to the synchronous write, which is the part that makes it a loop rather than a stale read.

I confirmed the reachability argument independently before changing anything: codePatchErrorMessage returns null at its first branch on applied, before touching the resource, and the template reads isDataLoaded / isLoadingDiff only inside the {{else}} of isAppliedOrIgnoredCodePatch. Resources are pull-based, so with no consumer reading it while applied, modify doesn't run and the branch is unreachable. Latent, as you say.

Worth naming the shape precisely, because it needs two calls: the first with applied has appliedStateChanged true, so it takes the inputsChanged branch and never reads the tracked state. It's the second call — same status, inputs unchanged — that reads all three, finds them falsy because the first load nulled them, and performs. That's the read-then-write inside one computation.

I ordered the new clause first rather than last:

if (
  codePatchStatus === 'applied' ||
  this.isDataLoaded ||
  this.errorMessage ||
  this.loadInFlight
) {

|| is left-to-right, so this returns without reading tracked state at all in the applied case — the entanglement never forms, instead of forming and then being harmless. Same four tokens.

I left the applied block at the top of loadDiff. With this change it's no longer load-bearing, but it's still reached on that first call and still does the nulling, so removing it would mean moving that clearing into modify — a bigger change than the problem justifies. It now reads as what it always claimed to be: documentation for why an applied patch has no diff to show.

Comment on lines +31 to +35
// Deliberately untracked: `modify` consults this to decide whether a load is
// already under way, and consuming tracked state there would re-enter
// `modify` every time the load changed it. The template reads the task's own
// `isRunning` instead, which is tracked and safe to render from.
private loadInFlight = false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] The comment states the right concern and then understates what the code does with it, which will mislead the next reader.

modify does consume tracked state, eight lines below: this.isDataLoaded reads the tracked originalCode and modifiedCode, and this.errorMessage is tracked. Both are written by load. So the resource is entangled with load state whether or not loadInFlight is tracked, and keeping this flag untracked buys something narrower than "we don't consume tracked state in modify" — it keeps a mid-flight signal out of the entanglement, so a load starting and finishing doesn't invalidate the cache twice on its own.

The invariant that actually makes the entanglement safe is worth writing down instead, because it is the one a future edit can break without noticing: every path in modify that reads isDataLoaded or errorMessage returns without writing them. Something like:

  // Untracked on purpose. `modify` runs inside the resource's tracked cache,
  // so anything it reads re-runs it when written. The load-completion state it
  // reads (`isDataLoaded`, `errorMessage`) is only ever read on paths that
  // return, so a completed load costs one extra `modify` and stops. A
  // mid-flight flag read the same way would add a second invalidation per
  // load, and a path that read either one and then started a load would
  // re-enter without bound.

Scope. Comment accuracy. Non-blocking — the code is right; the note next to it should describe the rule the code is following, since that rule is the whole safety argument. Related: the thread on the early return below, which is where that rule is closest to being broken.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] You're right, and the comment was worse than imprecise — it implied modify doesn't consume tracked state, when it does so eight lines below. Rewritten in ba5f454.

The version I landed states the invariant as the safety argument rather than as a property of the flag:

every path in modify that reads those returns without writing them, so a finished load costs one further modify and stops

with the narrower thing the untracked flag actually buys — sparing a second invalidation per load — kept as the secondary clause rather than the headline.

The pairing with the thread below is the useful part: that rule is what makes the entanglement safe, and the applied path was the one place it was about to be broken. Having fixed that, the comment now describes a rule the code actually keeps everywhere, which is the only kind worth writing down.

Comment on lines +194 to +206
let fileUrl = this.fileUrl;
if (!fileUrl) {
return '';
}
try {
return new URL(fileUrl).pathname.split('/').pop() || '';
} catch {
// The model names the file it is patching, and what it writes is not
// always a URL. Falling back to the last path segment keeps a header that
// is merely wrong from taking the whole message down with it; whether the
// patch can be applied is reported separately.
return fileUrl.split('/').pop() || '';
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] The guard is right, and the same raw string still reaches an unguarded new URL one component away.

Confirmation on this getter. The fallback is the correct shape: new URL('malformed file url') throws, and 'malformed file url'.split('/').pop() gives back the whole string, which is what the model wrote and therefore the most useful thing to show. The test pins exactly that.

Where it doesn't reach. this.file (the @cached getter above) builds its FileDef with sourceUrl: this.sourceUrl ?? '', and sourceUrl returns this.args.codeData.fileUrl verbatim for a non-new file — the same unparseable string. That FileDef goes straight into <AttachedFileDropdownMenu @file={{this.file}} …>, whose openInCodeMode does new URL(this.args.file.sourceUrl!) (packages/host/app/components/ai-assistant/attached-file-dropdown-menu.gts). The '' case is reachable too: sourceUrl returns null for a new file that isn't applied yet, so this.file.sourceUrl is '' and new URL('') throws the same way.

That is a click-time throw inside an @action, not a render-time one, so it does not undo what this change fixes — the message renders, which was the point. But the menu item is offered on a header whose file URL this change has just established may not be a URL, and choosing it throws.

Scope. Follow-up, non-blocking, and arguably pre-existing rather than introduced here — the '' path predates this change. Worth either guarding openInCodeMode the same way, or omitting the "open in code mode" item when sourceUrl doesn't parse, so the header degrades consistently instead of half-way.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] The main finding holds and I'm treating it as a follow-up rather than folding it in — but one detail of it doesn't survive checking, and it's the half that would have decided the fix.

Confirmed: openInCodeMode does new URL(this.args.file.sourceUrl!) with no guard, and for a non-new file sourceUrl hands it codeData.fileUrl verbatim. So the unparseable-but-non-empty case — malformed file url, exactly what this change made renderable — is offered as an enabled menu item that throws on click.

Not confirmed: the '' case. The menu item carries disabled: !this.args.file?.sourceUrl (attached-file-dropdown-menu.gts), and '' is falsy, so the new-file-not-yet-applied path you describe is already disabled rather than reachable. That matters for the remedy: "omit the item when sourceUrl doesn't parse" is the right shape, but it's an extension of a guard that exists, not a missing guard — the existing one just tests presence where it needs to test parseability.

Why not here. It's a different component, a click-time throw rather than a render-time one, and the fix wants its own test around the menu rather than the header. This branch has had a long CI road and I'd rather not widen it for something no current path reaches on render. Happy to do it as a follow-up — disabled: !parses(this.args.file?.sourceUrl) with a small helper is the whole change.

Agreed on the framing though: this change established that a header's file URL may not be a URL, so anything downstream taking that string as one is now on notice.

Comment on lines +369 to +378
{{#if @codeData.fileUrl}}
<codeBlock.diffEditorHeader
@codeData={{@codeData}}
@diffEditorStats={{null}}
@originalUploadedFileUrl={{@codePatchResult.originalUploadedFileUrl}}
@codePatchStatus={{@codePatchStatus}}
@userMessageThisMessageIsRespondingTo={{@userMessageThisMessageIsRespondingTo}}
@codePatchErrorMessage={{this.codePatchErrorMessage}}
/>
{{/if}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Non-blocking, two small things about this branch.

This {{#if}} is always true. isLoadingDiff requires this.load.isRunning, and modify performs the load only after if (!fileUrl) { this.errorMessage = 'Missing file URL in the code block'; return; }. So a running load implies a file URL, and the guard can go — one less branch to reason about in a template that already nests four deep. If it is here defensively, a comment saying so would keep the next reader from hunting for the case that needs it.

The state this branch exists to eliminate. Worth recording that it is now exhaustive, because that is the load-bearing property: after modify returns, the resource is always in exactly one of loaded (isDataLoaded), failed (errorMessage), or loading (load.isRunning) — the early return above only skips perform when one of the first two already holds, and every other path performs. The blank block this change removes was the fourth state, and there is no longer a way to reach it. That is the sentence I would want in the comment above rather than the rationale for the header, which is self-evident from the markup.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Both taken in ba5f454.

The guard is gone. Your reasoning checks out: modify reaches load.perform() only past if (!fileUrl) { this.errorMessage = …; return; }, so a running load implies a file URL and isLoadingDiff requires load.isRunning. It wasn't defensive, just redundant — and one less branch in a template already nesting four deep is worth having. Nothing crashes if the invariant were ever broken either, since fileName now falls back rather than constructing a URL blindly.

The comment says the exhaustiveness now. That is the better content, and I'd missed that it was the point:

Together with the branch above, this makes the states a patch can be rendered in exhaustive: once modify returns, the resource holds code, or an error, or a running load. There is no fourth state, and the empty block this branch replaced was it.

The header rationale is down to one clause explaining why it can always name its file, which is the only part not evident from the markup.

Worth noting these two threads compose: with applied now returning from modify, "code, error, or running load" is exhaustive over every path modify can take, not just the ones a consumer currently reaches.

Comment on lines +437 to +447
for (let i = 0; i < 5; i++) {
component.htmlParts = parseHtmlContent(codeBlockHtml, roomId, eventId);
await settled();
}

assert.strictEqual(
getSourceCallCount,
1,
'unchanged inputs do not send the realm another request',
);
assert.dom('.code-block-diff').exists('the diff survives re-rendering');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Strong test — the fresh-CodeData-with-identical-values setup is the right reproduction, and I checked that it exercises what it claims: searchReplaceBlock is only populated once isCompleteSearchReplaceBlock passes (packages/host/app/lib/formatted-message/utils.ts), so after a block completes the same string arrives on every re-parse and this.searchReplaceBlock !== searchReplaceBlock is false by value comparison. The whole fix rests on that being a string rather than a rebuilt object, and this test would fail the moment it stopped being one.

The arm that isn't covered is the one from the other half of the report. The description names two symptoms: the block sat holding no code, and the error was cleared along with it. This test pins the success arm of the early return — isDataLoaded → don't refetch. The errorMessage arm has no equivalent, and it is the arm that governs a failing load: a file that 404s or a realm that errors must be fetched once and then left alone, with the message still on screen after N re-renders. Right now, deleting this.errorMessage from the early-return condition leaves every test in this file green while restoring a request-per-invalidation storm against exactly the URLs that were failing — which is the shape that took staging down.

It is a small addition on top of what is already here:

cardService.getSource = async () => {
  getSourceCallCount++;
  throw new Error('realm unavailable');
};
// …render, then re-render with identical parts N times…
assert.strictEqual(getSourceCallCount, 1, 'a failed load is not retried on every invalidation');
assert.dom('[data-test-error-message]').exists('and its message survives');

Scope. Test coverage, non-blocking — but this is the cheapest of the suggestions here and it guards the more expensive failure.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Added in b888ce8 — you were right that this was the cheapest suggestion guarding the more expensive failure, so I did it first.

The test fetches once, fails, then re-renders five times with identical parts and asserts both halves: getSourceCallCount still 1, and [data-test-error-message] still on screen. Deleting this.errorMessage from the early-return condition now fails it, which was the gap — before this, that deletion was green.

Your point about why the existing test works is the part I'd want kept: searchReplaceBlock is a string populated only once isCompleteSearchReplaceBlock passes, so identical re-parses compare equal by value and fileOrPatchChanged stays false. The whole fix rests on that, and if it ever became a rebuilt object both tests would fail rather than silently stop testing anything — which is the right failure direction.

One caveat on the new test, stated plainly: it is typechecked and linted but unrun. Colima is down on this machine so the host suite can't start locally, and CI is the first thing to exercise it. Earlier in this branch's life two monaco-dependent tests passed review by reading and then failed on CI, so I've kept this one off the diff editor entirely — it asserts on the error alert and a call counter, nothing that depends on how monaco lays out panes.

backspace and others added 3 commits August 19, 2026 08:33
`modify` runs inside the resource's tracked cache, so what it reads there
re-runs it when written — and it reads load state. That is safe only because
every path reading it returns without writing. One state slipped through: a
load asked to run while the patch is already applied clears both codes and
returns, recording neither data nor an error, and it does so synchronously.
Reaching it left `modify` writing the state it had just read, from inside the
computation that read it — a cache dirtying itself on every read. Nothing in
the app gets there today; two guards in a component two files away are what
stand in the way, and neither is visible from here.

Return on `applied` instead, so it is a state `modify` settles in rather than
one a load is sent to abandon. State the entanglement invariant next to the
flag that depends on it, since an edit can break it without looking wrong.

In the template, drop the file-URL check around the loading header: a load only
runs once a URL is known, because `modify` records an error and returns without
performing when there isn't one. Say instead what that branch is for — with it,
the states a patch renders in are exhaustive.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The existing test pins the loaded arm: once a diff is in hand, re-rendering
does not fetch again. The failing arm had nothing on it, and it is the arm that
matters more — dropping `errorMessage` from the condition left every test in
the file green while restoring a request per invalidation against exactly the
URL that was already failing, and taking the message off screen with it.

Fetch once, fail, then re-render with identical parts and assert both: no
second request, and the error still shown.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants